Build an integration
PreviewConnect 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,callsandactions, 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:
| What | Looks like | Notes |
|---|---|---|
| Client id | rc_client_… | Public. Goes on the connect link |
| Client secret | rc_secret_… | Shown once. On your server only |
| Signing secret | whsec_… | 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
Send the merchant to RowCall
Make a random
stateand 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=S256Leave out
scopeto ask for every scope your app is registered for. You can ask for fewer, never more.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.
They come back to you
RowCall redirects to your redirect URI with
codeand yourstate. Check thatstatematches the one you kept. When they cancel you geterror=access_deniedwith yourstate; a bad request gets another RFC 6749 error, such asinvalid_scope, with anerror_description.GET /rowcall/callback?code=rc_code_…&state=9f2c…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.
| Token | Lasts | Notes |
|---|---|---|
Access token rc_at_… | 1 hour | Sent on every API request |
Refresh token rc_rt_… | 90 days | Works 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_orderorupdate_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.
Everything a run request can carry.