How to create an account and log in
Intro
Create Omnisend accounts for your merchants, log them in, and manage their brand — all via API. This guide walks through the partner integration flow: obtaining an access token, creating an account, establishing a login session, and (optionally) updating brand details.
See API reference: Accounts
Authentication model — read this first
This flow uses two different OAuth credentials. They are not interchangeable.
| Credential | Grant | Bound to | Use it for | Typical scopes |
|---|---|---|---|---|
| Partner credentials | client_credentials | your platform (no brand) | Creating accounts, creating login sessions (Steps 1–4) | accounts.write, sessions.write |
| Brand app | authorization_code (+ refresh_token) | a single brand | Any brand-scoped API: updating the brand, contacts, products, events, etc. (Step 5 and beyond) | brands.write, contacts.write, products.write, events.write, … |
Why two? Account and session creation happen before a brand exists, so they use a platform-level credential identified by your partner ID. Brand-scoped calls act on a specific brand, so they require a token bound to that brand. A client_credentials token has no brand attached and cannot be used for brand-scoped endpoints — adding brands.write to it will not work (see Troubleshooting).
Note: A brand access token is obtained after the user has logged in (Step 4), by running the standard OAuth authorization flow for your brand app (Step 5).
Prerequisites
- Partner credentials (
client_idandclient_secret) for theclient_credentialsgrant — contact Omnisend to obtain them. - A brand app (
client_idandclient_secret, with a registeredredirect_uri) for theauthorization_codegrant, if you need to make brand-scoped calls — contact Omnisend to set it up. - Your platform must be registered as an Omnisend partner.
Step 1: Get a partner access token
Request an access token using the OAuth 2.0 client credentials grant with your partner credentials. Request only the scopes used by Steps 2–4.
curl --request POST \
--url 'https://app.omnisend.com/oauth2/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=YOUR_PARTNER_CLIENT_ID' \
--data-urlencode 'client_secret=YOUR_PARTNER_CLIENT_SECRET' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'scope=accounts.write sessions.write'Response:
{
"access_token": "YOUR_PARTNER_ACCESS_TOKEN",
"expires_in": 7200,
"token_type": "Bearer",
"scope": "accounts.write sessions.write"
}Note: The access token expires after 2 hours (
expires_in: 7200seconds). Request a new token when the current one expires.
Partner-credential scopes
| Scope | Use |
|---|---|
| accounts.write | Create accounts (Step 2) |
| sessions.write | Create login tokens / sessions (Steps 3–4) |
Note:
brands.writeis not a partner-credential scope. It belongs to your brand app and is used with a brand access token in Step 5. Requestingbrands.writehere has no effect on brand-scoped endpoints.
Step 2: Create an account
Create a new Omnisend account with business details, a primary user, and an address. Save the returned accountID, brandID, and userID — you will need them to log the user in.
curl --request POST \
--url https://api.omnisend.com/api/accounts \
--header 'Authorization: Bearer YOUR_PARTNER_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data '{
"name": "My Store",
"website": "https://www.mystore.com",
"timezone": "Europe/Vilnius",
"currency": "EUR",
"language": "en",
"user": {
"email": "[email protected]",
"firstName": "Jane",
"lastName": "Smith"
},
"address": {
"countryCode": "US",
"stateCode": "CA",
"city": "San Francisco",
"address": "123 Market St.",
"postCode": "94105"
}
}'Response (201 Created):
{
"accountID": "60a6170920d91c215e949b5a",
"brandID": "60a6170920d91c215e949b5c",
"userID": "60a6170920d91c215e949b5d"
}Warning: If
countryCodeisUS, thestateCodefield is required. Omitting it returns a400validation error.
Field validations
| Field | Format |
|---|---|
| website | Valid URL |
| timezone | IANA Time Zone (e.g. Europe/Vilnius) |
| currency | ISO 4217 (e.g. EUR, USD) |
| language | BCP 47 subtag (e.g. en, de) |
| address.countryCode | ISO 3166-1 alpha-2 (e.g. US, LT) |
| address.stateCode | 2 letters, required if countryCode is US |
Step 3: Create a login token
From your backend, using the partner access token, request a one-time login token with the brandID and userID from account creation:
curl --request POST \
--url https://api.omnisend.com/api/sessions/token \
--header 'Authorization: Bearer YOUR_PARTNER_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data '{
"brandID": "60a6170920d91c215e949b5c",
"userID": "60a6170920d91c215e949b5d"
}'Response:
{
"token": "ONE_TIME_LOGIN_TOKEN"
}Note: The login token is single-use. Generate a new token each time you want to log the user in.
Step 4: Log the user in
From your frontend, POST the login token to the sessions endpoint. This must happen in the user's browser so that session cookies are properly set.
Submit a form POST (or use JavaScript to submit a form) to https://api.omnisend.com/api/sessions:
<form method="POST" action="https://api.omnisend.com/api/sessions">
<input type="hidden" name="token" value="ONE_TIME_LOGIN_TOKEN" />
<input type="hidden" name="returnUrl" value="https://app.omnisend.com/" />
<button type="submit">Open Omnisend</button>
</form>On success, the user is redirected to Omnisend with active session cookies.
| Parameter | Required | Description |
|---|---|---|
| token | Yes | One-time login token from Step 3 |
| returnUrl | No | Redirect URL after login. Must start with https://app.omnisend.com/ |
Note: The
True-Client-IPheader with the end user's IP address and a validUser-Agentheader are required. Most browsers and HTTP clients setUser-Agentautomatically.
Step 5: Make brand-scoped calls (optional)
Brand-scoped endpoints — such as updating the brand, or managing contacts, products, and events — require a brand access token issued to your brand app, not the partner token from Step 1. A brand access token is obtained with the authorization_code grant, which binds the token to the specific brand the user is logged into.
Prerequisite: The user must be logged in (Step 4 completed) in the same browser session. The authorization step relies on that session to determine which brand the token is bound to.
Step 5a: Authorize your brand app
Redirect the user's browser to the authorization endpoint with your brand app client_id. Because the user already has an active session from Step 4, no extra login is needed; if your brand app is configured to skip consent, this redirect is transparent.
GET https://app.omnisend.com/oauth2/authorize
?client_id=YOUR_BRAND_APP_CLIENT_ID
&redirect_uri=YOUR_REGISTERED_REDIRECT_URI
&response_type=code
&scope=brands.write
&state=RANDOM_STATE
&nonce=RANDOM_NONCE
On success, the browser is redirected to your redirect_uri with an authorization code (and the state you sent, which you must verify).
Step 5b: Exchange the code for a brand access token
From your backend, exchange the authorization code for an access token and a refresh token:
curl --request POST \
--url 'https://app.omnisend.com/oauth2/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=YOUR_BRAND_APP_CLIENT_ID' \
--data-urlencode 'client_secret=YOUR_BRAND_APP_CLIENT_SECRET' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code=AUTHORIZATION_CODE' \
--data-urlencode 'redirect_uri=YOUR_REGISTERED_REDIRECT_URI'Response:
{
"access_token": "YOUR_BRAND_ACCESS_TOKEN",
"refresh_token": "YOUR_REFRESH_TOKEN",
"token_type": "Bearer",
"scope": "brands.write"
}This access_token is bound to the brand and carries the scopes you requested. Use it for all brand-scoped calls, and store the refresh_token to obtain new access tokens without repeating the browser flow (see Step 5d).
Step 5c: Update brand information
Send a PATCH request with only the fields you want to change, using the brand access token:
curl --request PATCH \
--url https://api.omnisend.com/api/brands/current \
--header 'Authorization: Bearer YOUR_BRAND_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data '{
"name": "Updated Store Name",
"timezone": "America/New_York"
}'Response: 204 No Content
Step 5d: Refresh the brand token
Brand access tokens expire. When one expires, use the refresh_token from Step 5b to obtain a new brand access token from your backend — no browser flow needed:
curl --request POST \
--url 'https://app.omnisend.com/oauth2/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=YOUR_BRAND_APP_CLIENT_ID' \
--data-urlencode 'client_secret=YOUR_BRAND_APP_CLIENT_SECRET' \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode 'refresh_token=YOUR_REFRESH_TOKEN'Response:
{
"access_token": "NEW_BRAND_ACCESS_TOKEN",
"token_type": "Bearer",
"scope": "brands.write"
}Note: The response returns a new
access_tokenbut no newrefresh_token— keep reusing your existing refresh token for subsequent refreshes.
Complete flow summary
- Get partner access token →
POST https://app.omnisend.com/oauth2/token(client credentials,accounts.write sessions.write) - Create account →
POST https://api.omnisend.com/api/accounts(partner token; savebrandID,userID) - Create login token →
POST https://api.omnisend.com/api/sessions/token(partner token, from backend) - Log user in →
POST https://api.omnisend.com/api/sessions(from browser) - Brand-scoped calls (optional):
- 5a. Authorize brand app →
GET https://app.omnisend.com/oauth2/authorize(browser, after login) - 5b. Exchange code →
POST https://app.omnisend.com/oauth2/token(authorization_code → brand token + refresh token) - 5c. Update brand →
PATCH https://api.omnisend.com/api/brands/current(brand token) - 5d. Refresh →
POST https://app.omnisend.com/oauth2/token(refresh_token → new brand token)
- 5a. Authorize brand app →
Troubleshooting
403 Forbidden on PATCH /api/brands/current with "This action requires the 'brands.write' scope."
Your token does not carry brands.write. This scope belongs to the brand app (Step 5), not the partner credentials. Adding brands.write to the client_credentials request in Step 1 will not fix it — see below.
403 Forbidden on PATCH /api/brands/current with "The server understood the request, but is refusing to fulfill it."
You are calling a brand-scoped endpoint with a partner (client_credentials) token. That token has no brand attached, so the request is rejected even if it carries brands.write. Use a brand access token obtained via the authorization_code flow in Step 5.
Updated 27 days ago