Self-Onboarding
API Reference — Stores API Reference — Webhooks
Self-onboarding allows POS integrators to associate new stores to their integration autonomously, without requiring manual intervention per store. Once a TAM creates the Integration and clientId in Auth0, your system can provision and deprovision stores programmatically at any scale.
Prerequisites
Before using self-onboarding, a Technical Account Manager (TAM) must perform a one-time setup:
- Create the
Integrationentity in the Rappi backend. - Create the
clientIdin Auth0 associated with your integration.
After this setup, all provisioning operations are fully self-service.
Authentication
Self-onboarding uses a two-token model. Your application needs to obtain two independent tokens before calling any self-onboarding endpoint:
Integrator token (your credentials)
Obtain a machine-to-machine (M2M) token using your client_id and client_secret. For details on how to obtain this token, see the Authentication guide.
This token is long-lived (configurable TTL). Cache it and only refresh when it expires.
Merchant token (your merchant's credentials)
To access a merchant's stores, your application must redirect the merchant through Rappi's OAuth2 Authorization Code + PKCE flow on Portal Partners. This authorizes your integration to act on the merchant's behalf.
| Environment | Authorization URL | Token URL |
|---|---|---|
| Production | https://login.partners.rappi.com/authorize | https://login.partners.rappi.com/oauth/token |
| Development | https://login.partners.dev.rappi.com/authorize | https://login.partners.dev.rappi.com/oauth/token |
ℹ️ The examples below use the Production environment. Replace the URLs with the Development endpoints shown above when testing.
⚠️ The
client_idand yourredirect_urimust be registered with the Rappi integrations team. Contact your TAM.
Step 1 — Generate a PKCE code challenge
Before redirecting, your backend must generate a PKCE pair:
- Generate a cryptographically random
code_verifier(43–128 URL-safe characters) - Compute
code_challenge = BASE64URL(SHA256(code_verifier))
Step 2 — Redirect the merchant
Build the authorization URL and redirect the merchant's browser:
GET https://login.partners.rappi.com/authorize ?client_id=YOUR_CLIENT_ID &redirect_uri=YOUR_REDIRECT_URI &response_type=code &scope=openid profile email &code_challenge=YOUR_CODE_CHALLENGE &code_challenge_method=S256 &state=YOUR_RANDOM_STATE
⚠️ Always validate that the
statevalue in the redirect matches what you sent — this prevents CSRF attacks.
Step 3 — Merchant authenticates
The merchant logs in with their Rappi Portal Partners credentials. Once done, Portal Partners redirects back to your redirect_uri with an authorization code:
GET YOUR_REDIRECT_URI?code=AUTHORIZATION_CODE&state=YOUR_RANDOM_STATE
Step 4 — Exchange the code for a merchant JWT
Your backend exchanges the authorization code for a merchant JWT:
POST https://login.partners.rappi.com/oauth/token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code &code=AUTHORIZATION_CODE &client_id=YOUR_CLIENT_ID &code_verifier=YOUR_CODE_VERIFIER &redirect_uri=YOUR_REDIRECT_URI
ℹ️ No
client_secretis required — this is a public OAuth2 client using PKCE.
The response includes the merchant JWT. This token contains the merchant's email in its claims.
Step 5 — Use both tokens on every request
| Header | Value | Purpose |
|---|---|---|
Authorization | Bearer <integrator JWT> | Identifies your integration |
Authorization-Partners | Bearer <merchant JWT> | Grants access to the merchant's stores |
Step-by-Step Flow
Step 1 — Configure the webhook (once)
Register a webhook URL for your integration so Rappi can notify you when a provisioning operation completes. This is a one-time setup per integration.
Use the POST /clients/{clientId}/webhooks endpoint:
POST /clients/{clientId}/webhooks
Request body:
{ "event": "STORE_PROVISIONING_STATUS", "url": "https://your-endpoint.com/rappi/events", "secret": "your-hmac-secret" }
The event field specifies which event to configure (currently only STORE_PROVISIONING_STATUS is supported). The secret field is optional. If provided, Rappi will sign each webhook payload with HMAC-SHA256 and include the signature in the Rappi-Signature header so you can verify the request origin. If omitted, no signature will be sent — provide your own secret and store it securely if you need to verify webhook authenticity.
A 201 Created response confirms the webhook is configured (or 200 OK if updating an existing configuration). From this point on, all provisioning and deprovisioning results for your integration will be delivered to this URL.
Step 2 — Retrieve Stores
Before provisioning, verify which stores are already integrated and which are not. This avoids duplicate provisioning requests.
Use the GET /v2/stores/integration-status endpoint:
GET /v2/stores/integration-status
Headers:
| Header | Value |
|---|---|
Authorization | Bearer <integrator JWT> (M2M, Auth0) |
Authorization-Partners | Bearer <merchant JWT> (OAuth2, Auth0) |
The store IDs are fetched automatically from the merchant's JWT email — no request body is needed.
Response:
{ "stores": [ { "storeId": "1", "name": "Your Brand Main", "brand": "YourBrand", "integrated": true, "integrationId": "your-integration-id", "children": [ { "storeId": "3", "name": "Child Store 3", "brand": "YourBrand", "integrated": true, "integrationId": "your-integration-id" }, { "storeId": "4", "name": "Child Store 4", "brand": "YourBrand", "integrated": false } ] }, { "storeId": "2", "name": "Your Brand Secondary", "brand": "YourBrand", "integrated": true, "integrationId": "your-integration-id", "children": [] }, { "storeId": "10", "name": "Sertester1", "brand": "YourBrand", "integrated": false, "children": [ { "storeId": "20", "name": "Sertester1 Child 1", "brand": "YourBrand", "integrated": false }, { "storeId": "21", "name": "Sertester1 Child 2", "brand": "YourBrand", "integrated": false } ] }, { "storeId": "11", "name": "FIFOUno", "brand": "YourBrand", "integrated": false, "children": [] } ] }
Proceed to provision only the stores where integrated is false. Each entry includes storeId, name, and brand to help you identify the store. The children array reflects the hierarchy — each child also carries an integrated flag and, when integrated, its integrationId.
Step 3 — Activation
Submit a provisioning request for the stores that are not yet integrated. This operation is asynchronous — the API returns 202 Accepted immediately and the result is delivered via webhook. You can provision up to 20 stores per request.
Use the POST /v2/stores/provisioning endpoint:
POST /v2/stores/provisioning
Headers:
| Header | Value |
|---|---|
Authorization | Bearer <integrator JWT> (M2M, Auth0) |
Authorization-Partners | Bearer <merchant JWT> (OAuth2, Auth0) |
Request body:
{ "stores": [ { "storeId": "10", "name": "My Store", "integrationId": "your-integration-id" }, { "storeId": "11", "name": "My Other Store", "integrationId": "your-integration-id" } ] }
All three fields — storeId, name, and integrationId — are required per store item. Stores missing any of these fields will be rejected. The brand is resolved automatically from Portal Partners based on the merchant's credentials.
The following fields are optional and control integration behavior. If not provided, the defaults shown below are applied:
| Field | Type | Default | Description |
|---|---|---|---|
pingActive | boolean | false | Enable ping events |
getMenuActive | boolean | false | Enable get-menu events |
cancellationEvents | boolean | false | Enable cancellation events |
status | string | INACTIVE | Initial store status. Use ACTIVE to activate immediately on provisioning |
Response: 202 Accepted
{ "batchId": "550e8400-e29b-41d4-a716-446655440000", "accepted": [ { "storeId": "10", "integrationId": "your-integration-id" } ], "rejected": [ { "storeId": "11", "reason": "not_owned" } ] }
Possible rejected[].reason values:
| Reason | Meaning |
|---|---|
not_owned | The store does not belong to the authenticated merchant. |
missing_name | The name field is missing or blank. |
invalid_integration_id | The integrationId field is missing or blank. |
If all stores are rejected (none owned by the merchant), the API returns 422 Unprocessable Entity.
Step 4 — Receive the webhook
When the provisioning operation completes, Rappi sends a STORE_PROVISIONING_STATUS event to the URL configured in step 1.
See STORE_PROVISIONING_STATUS for the full payload reference.
Example payload:
{ "batchId": "550e8400-e29b-41d4-a716-446655440000", "integrationId": "your-integration-id", "operation": "PROVISION", "results": [ { "storeId": "10", "integrationId": "your-integration-id", "brand": "YourBrand", "status": "ACTIVE", "httpCode": 201 }, { "storeId": "11", "integrationId": "your-integration-id", "brand": "YourBrand", "status": "FAILED", "errorMessage": "Store already exists", "httpCode": 409 } ], "timestamp": "2026-04-21T10:00:00Z" }
Possible results[].status values:
| Status | Meaning |
|---|---|
ACTIVE | Provisioning completed successfully. The store is now integrated. |
INACTIVE | Deprovisioning completed successfully. |
FAILED | The operation failed. Check errorMessage and httpCode for details. |
results[].httpCode is the HTTP status code of the underlying operation (e.g. 201 for success, 409 for conflict, 204 for deprovisioning).
operation is either PROVISION or DEPROVISION.
Step 5 — Deactivating (optional)
To remove stores from your integration, use the POST /v2/stores/deprovisioning endpoint. Like provisioning, this is asynchronous — the result is delivered via the same STORE_PROVISIONING_STATUS webhook with operation: DEPROVISION and status: INACTIVE per store.
POST /v2/stores/deprovisioning
Headers:
| Header | Value |
|---|---|
Authorization | Bearer <integrator JWT> (M2M, Auth0) |
Authorization-Partners | Bearer <merchant JWT> (OAuth2, Auth0) |
Request body:
{ "stores": [ { "storeId": "10", "integrationId": "your-integration-id" } ] }
Both fields — storeId and integrationId — are required per store item.
Possible rejected[].reason values:
| Reason | Meaning |
|---|---|
not_integrated | The store is not currently integrated. |
invalid_integration_id | The integrationId does not match the store's current integration. |
Response: 202 Accepted with the same batch body as provisioning.
If all stores are rejected, the API returns 422 Unprocessable Entity.
Full Flow Summary
[Once] Configure webhook → POST /clients/{clientId}/webhooks [Per batch] Retrieve Stores → GET /v2/stores/integration-status Activation → POST /v2/stores/provisioning (up to 20 stores per request) Await result → STORE_PROVISIONING_STATUS webhook [Optional] Deactivating → POST /v2/stores/deprovisioning
Error Response Format
All error responses from this API use a consistent JSON structure:
{ "message": "Human-readable description of the error" }
HTTP Status Codes
| Status | When |
|---|---|
400 Bad Request | Invalid request — missing fields, empty list, exceeds 20 stores, mixed integrations in batch |
401 Unauthorized | Missing or invalid integrator token |
403 Forbidden | Merchant token does not match the integration's integrationId, or merchant not authorized in Partners |
422 Unprocessable Entity | All stores were rejected (none owned by the merchant) |
424 Failed Dependency | Partners service is unavailable |
500 Internal Server Error | Unexpected server error |
Example — exceeding the store limit:
HTTP/1.1 400 Bad Request { "message": "stores list exceeds maximum allowed size of 20" }
Example — merchant not authorized:
HTTP/1.1 403 Forbidden { "message": "Merchant not authorized in Partners" }
