Now inviting stores. Join the waitlist to hear the agent call you.

Join the waitlist

Search the docs

Find a docs page

API reference

Build an integration

Preview

Connect many merchants' RowCall orgs to your platform over OAuth, set each one up by API, and hear their events and actions at one URL.

An API key belongs to one org. A platform such as Glood has many merchants, each with their own RowCall org, so it connects to them over OAuth 2.0 instead. Each merchant clicks Connect once; your app then gets tokens that act for that merchant's org only, with the scopes they agreed to.

RowCall is the authorization server. It supports the authorization code grant with PKCE (S256) and refresh tokens, nothing else: no implicit or password grant.

Register your app

RowCall staff register your app. Ask us, and send:

  • Redirect URIs. Where merchants come back after connecting. Matched exactly, so list each one in full.
  • Scopes. What your app may ask for: runs, flows, contacts, calls and actions, as in the scopes table.
  • Webhook URL and the event types it should receive, if you want events.
  • Action URL, if your app answers actions during calls.

You get back:

WhatLooks likeNotes
Client idrc_client_…Public. Goes on the connect link
Client secretrc_secret_…Shown once. On your server only
Signing secretwhsec_…Shown once. Checks the webhooks and action requests RowCall sends you

Test and live are separate apps, each with its own client id and secrets. A test app's tokens work in test mode: their runs never dial. Build against the test app first.

Connect a merchant

Connect a partner app: press Connect RowCall on Glood, approve what Glood may do on RowCall’s consent page, and Glood can start runs for your store.
  1. Send the merchant to RowCall

    Make a random state and a PKCE verifier, keep both with the merchant's session, and redirect them to the authorize page with the verifier's SHA-256 challenge.

    GET /oauth/authorize?response_type=code&client_id=rc_client_…&redirect_uri=https%3A%2F%2Fpartner.example.in%2Frowcall%2Fcallback&scope=runs%20flows%20actions&state=9f2c…&code_challenge=E9Mel…&code_challenge_method=S256

    Leave out scope to ask for every scope your app is registered for. You can ask for fewer, never more.

  2. The merchant connects

    They sign in to RowCall (a trusted app's merchants can create their account and org right there), pick the org, see what your app will be able to do, and choose Connect or Cancel. Only people who manage the org's developer settings, owners and admins, can connect it.

  3. They come back to you

    RowCall redirects to your redirect URI with code and your state. Check that state matches the one you kept. When they cancel you get error=access_denied with your state; a bad request gets another RFC 6749 error, such as invalid_scope, with an error_description.

    GET /rowcall/callback?code=rc_code_…&state=9f2c…
  4. Swap the code for tokens

    Post the code to the token endpoint at once: a code lives 60 seconds and works once. See below.

If the link has a client id or redirect URI RowCall doesn't know, the merchant sees an error on RowCall and isn't sent back.

Tokens

The token endpoint takes a form-encoded body. Authenticate with your client id and secret by HTTP Basic, or send them as client_id and client_secret in the body.

POST /v1/oauth/token
Authorization: Basic cmNfY2xpZW50X…
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=rc_code_…&redirect_uri=https%3A%2F%2Fpartner.example.in%2Frowcall%2Fcallback&code_verifier=dBjftJeZ4CVP…
{
  "access_token": "rc_at_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "rc_rt_…",
  "scope": "actions flows runs",
  "org": { "id": "org_t1y3u5i7o9p1a3s5d7f9", "name": "Halden" }
}

org.id is the merchant's RowCall org. Store the refresh token against it. A code sent a second time is refused, and the tokens it already issued are revoked.

TokenLastsNotes
Access token rc_at_…1 hourSent on every API request
Refresh token rc_rt_…90 daysWorks once. Each refresh returns a new pair

Refreshing

POST /v1/oauth/token
Authorization: Basic cmNfY2xpZW50X…
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=rc_rt_…

The answer has the same shape, with a new refresh token. Add scope to ask for fewer scopes than the merchant granted, never more.

Revoking

POST /v1/oauth/revoke
Authorization: Basic cmNfY2xpZW50X…
Content-Type: application/x-www-form-urlencoded

token=rc_rt_…

Revocation follows RFC 7009 and always answers 200. Revoking a refresh token stops the access tokens issued with it too.

Errors and metadata

The OAuth endpoints use RFC 6749 errors, not RowCall's ok wrapper: { "error": "invalid_grant", "error_description": "The code has expired." }. A wrong client id or secret is 401 invalid_client; the rest are 400. GET /.well-known/oauth-authorization-server lists every endpoint and what's supported (RFC 8414).

Call the API for a merchant

Every /v1 endpoint accepts an access token in place of an API key, and acts for that one org with the scopes granted and your app's mode. GET /v1/me needs no scope and tells you which org a token acts for, with caller.kind: "app", your app's name, its mode and the granted scopes. A request outside the granted scopes gets 403 FORBIDDEN. An expired or revoked token, or one from a merchant who disconnected your app, gets 401 UNAUTHENTICATED: refresh once, and if that fails too, ask the merchant to connect again.

Set a merchant up

A new merchant has no flows yet. Install one from RowCall's ecommerce library with POST /v1/flows/library/{libraryKey}/install (flows scope):

curl -X POST https://app.rowcall.ai/v1/flows/library/order_verification/install \
  -H "Authorization: Bearer $ACCESS_TOKEN"

RowCall copies the library flow into the merchant's org and publishes it. Installing again returns the same flow with isNew: false, so it's safe to call every time a merchant connects. Every flow in /v1 carries its libraryKey, or null when the merchant built it themselves.

Then start runs with the library key as flowKey instead of a flowId, so you never need to store each merchant's flow ids: "flowKey": "order_verification" in the run request. Send a flowId or a flowKey, not both. The rest of the request is as in Start a run.

Build flows and actions for a merchant

Your platform can do more than install library flows. It can define the actions it answers and make the merchant's flows itself, so the merchant never opens the flow builder. Everything you make shows in the merchant's RowCall as made by your app.

Actions (actions scope). Define an action your action URL answers with PUT /v1/actions/{name}, change it, or delete it:

PUT /v1/actions/check_loyalty_points
Authorization: Bearer rc_at_…
Content-Type: application/json

{
  "description": "Looks up the shopper's loyalty points.",
  "kind": "read",
  "parameters": { "phone": { "type": "string", "description": "The shopper's phone." } },
  "sample": { "points": 120 },
  "transport": "rowcall"
}

DELETE /v1/actions/check_loyalty_points removes it. It's refused while a published flow uses it. See Actions.

Flows (flows scope). Make a flow from a whole definition with POST /v1/flows, the same shape the flow builder saves (GET /v1/flows/{flowId}/draft on any flow shows one):

POST /v1/flows
Idempotency-Key: cod-check-merchant-42

{ "definition": { "name": "COD check", "callType": "transactional", "languages": { "…": "…" }, "inputs": [], "steps": [], "outcomes": [], "actions": ["confirm_order"] } }

A half-written definition is saved as it is, and the draft's problems list what still stops it publishing, with the path to each, such as ["steps"]: "Add at least one step." Fix them with PUT /v1/flows/{flowId}/draft (the whole definition again), then publish. Publishing is refused with 422 FLOW_INVALID while there are problems. Once published, the draft becomes the version runs use. A new draft of a published flow leaves the published version running until you publish again. Try a flow with a test credential first: test runs walk it without calling anyone.

Webhooks and actions for every merchant

You don't add endpoints per merchant. RowCall sends every connected org's events, of the types your app subscribes to, to your one webhook URL, and their action requests to your one action URL. Both are signed with your app's signing secret, with the same RowCall-Signature as webhooks and actions, and every body carries orgId so you know which merchant it's about.

The body is the same as any event; use its orgId to pick the merchant's stored tokens and records.

  • If a merchant set their own action URL in Developers → Actions, their URL wins and your app doesn't get their action requests.
  • The suggested actions (such as confirm_order or update_delivery_address) come to your action URL even when the merchant never saved them, with their default settings.

Two events always come to your webhook URL, whatever it subscribes to: app.installed when a merchant connects your app for the first time, and app.uninstalled when they disconnect it, sent even though your access has already stopped. Both carry your app and the scopes.

Disconnecting

Merchants see your app in Settings → Connected apps: what it may use, who connected it and when it was last used. When they disconnect, your tokens stop working at once, events and action requests for that org stop, and you hear app.uninstalled. Delete the tokens you stored for that org. To disconnect from your side, revoke the refresh token.

With the SDK

The TypeScript SDK does the OAuth steps for you. RowCallOAuth builds the connect link and swaps and refreshes tokens; new RowCall({ accessToken, refresh }) refreshes once by itself when a request gets 401, then retries.

import { randomBytes } from 'node:crypto'
import express from 'express'
import { RowCall, RowCallOAuth } from '@rowcall/sdk'

// `db` and `req.session` stand for your own storage.
const baseUrl = process.env.ROWCALL_URL
const oauth = new RowCallOAuth({
  clientId: process.env.ROWCALL_CLIENT_ID,
  clientSecret: process.env.ROWCALL_CLIENT_SECRET,
  redirectUri: 'https://partner.example.in/rowcall/callback',
  baseUrl,
})

/** A client for one merchant's org that refreshes its token by itself. */
const rowcallFor = async (orgId: string) => {
  const stored = await db.rowcallTokens.get(orgId)
  return new RowCall({
    accessToken: stored.accessToken,
    baseUrl,
    refresh: async () => {
      const latest = await db.rowcallTokens.get(orgId)
      const tokens = await oauth.refresh(latest.refreshToken)
      await db.rowcallTokens.save(orgId, { accessToken: tokens.access_token, refreshToken: tokens.refresh_token })
      return tokens.access_token
    },
  })
}

const app = express()

// The merchant clicks "Connect RowCall" in your app.
app.get('/rowcall/connect', (req, res) => {
  const state = randomBytes(16).toString('hex')
  const { verifier, challenge } = RowCallOAuth.pkcePair()
  req.session.rowcall = { state, verifier }
  res.redirect(oauth.authorizeUrl({ state, codeChallenge: challenge, scope: ['runs', 'flows', 'actions'] }))
})

// RowCall sends them back here.
app.get('/rowcall/callback', async (req, res) => {
  const { state, verifier } = req.session.rowcall ?? {}
  if (!state || req.query.state !== state) return res.status(400).send('Please connect again.')
  if (req.query.error) return res.redirect('/settings?rowcall=cancelled')

  const tokens = await oauth.exchangeCode({ code: String(req.query.code), codeVerifier: verifier })
  await db.rowcallTokens.save(tokens.org.id, { accessToken: tokens.access_token, refreshToken: tokens.refresh_token })

  const rowcall = await rowcallFor(tokens.org.id)
  await rowcall.flows.install('order_verification')
  res.redirect('/settings?rowcall=connected')
})

// Later, when the merchant gets a cash-on-delivery order.
export const verifyOrder = async (orgId: string, order: Order) => {
  const rowcall = await rowcallFor(orgId)
  return rowcall.runs.start(
    {
      flowKey: 'order_verification',
      phone: order.phone,
      name: order.customerName,
      inputs: { order_id: order.id, items: order.summary, order_total: order.total, address: order.address },
      consent: { scope: 'service', source: 'checkout', givenAt: order.consentAt, note: 'Checkbox at checkout' },
      metadata: { orderId: order.id },
    },
    { idempotencyKey: `verify-${order.id}` },
  )
}

RowCallOAuth has pkcePair(), authorizeUrl({ state, codeChallenge, scope }), exchangeCode({ code, codeVerifier }), refresh(refreshToken, scope?) and revoke(token). The rest of the client is the same as with an API key; each endpoint's SDK call is in the API reference.

Check what RowCall sends with verifyWebhook and verifyActionRequest, passing your app's signing secret.

Next stepStart a run

Everything a run request can carry.