OAuth 2.0 with PKCE
Authorization code flow with mandatory PKCE (S256): authorize URL, consent, callback, token exchange, refresh, revoke and every error.
ColiSwift implements the OAuth 2.0 authorization code grant (RFC 6749) with PKCE S256 (RFC 7636) required for every client, refresh-token rotation, and token revocation (RFC 7009). No other grant type exists: no client credentials, no implicit, no password grant.
| Lifetime | Value |
|---|---|
| Authorization code | 5 minutes, single use |
Access token (cs_at_…) | expires_in seconds — 3600 (1 hour) by default |
Refresh token (cs_rt_…) | 90 days by default, single use (rotated on every refresh) |
1. Authorize URL
https://coliswift.com/oauth/authorizeresponse_typestringrequiredOnly
codeis supported.code
client_idstringrequiredYour
cs_client_…id.redirect_uristringrequiredMust be byte-for-byte one of your registered URIs. A trailing slash makes it a different URI. There is no default.
scopestringSpace-separated scopes. Omitted = every scope your app is allowed. Present = exactly those; an unknown scope or one your app is not allowed is refused (never silently dropped).
statestringrequiredYour random anti-CSRF value; returned unchanged. Required and non-empty.
code_challengestringrequiredBASE64URL(SHA256(code_verifier)), no padding (43–128 chars ofA-Z a-z 0-9 - . _ ~).code_challenge_methodstringrequiredplainis refused.S256
The verifier is 43–128 characters of the same unreserved set; 32 random bytes in base64url (43 characters) is the usual choice. Code to generate both is in the Quickstart.
2. Consent
- A signed-out visitor is sent to the ColiSwift login and back to the consent page with the request intact.
- Only an active seller account can approve. A staff account sees « Only a seller account can connect an application » (
OAUTH_NOT_A_SELLER); so does a seller whose account is not active. - The page shows your app’s name and logo, the seller’s identity and one sentence per requested scope. If the seller already connected your app it says so.
- The seller clicks approve or deny.
Errors shown on the page (never redirected)
A malformed request is answered on the ColiSwift page and the browser is not sent back to you — redirecting to an unverified address would make the endpoint an open redirector. Fix the link if you see one of these while testing:
| Code | Cause |
|---|---|
OAUTH_INVALID_CLIENT | client_id missing, unknown, or the app is deactivated. |
OAUTH_INVALID_REDIRECT_URI | redirect_uri missing or not exactly registered. |
OAUTH_UNSUPPORTED_RESPONSE_TYPE | response_type is not code. |
OAUTH_INVALID_REQUEST | state missing; code_challenge_method not S256; code_challenge missing or malformed. |
OAUTH_INVALID_SCOPE | An unknown scope, an empty scope=, or a scope outside your app’s allowed list. |
OAUTH_NOT_A_SELLER | Signed in as staff, or the seller account is not active. |
OAUTH_APP_PRIVATE | A sandbox developer app opened by a seller other than the one who created it. Only that account can connect it; nothing is shared. |
3. Callback
After the decision the browser lands on your redirect_uri:
GET https://app.example.com/coliswift/callback?code=cs_code_…&state=YOUR_STATEGET https://app.example.com/coliswift/callback?error=access_denied&state=YOUR_STATEAlways compare state with the value in the user’s session before doing anything else.
Connecting the same seller again
There is one connection (grant) per app and seller. A second approval reuses it: nothing is revoked, existing tokens keep working, and the grant’s scopes become the union of the old and the new ones. The tokens minted from the new code carry the scopes of that consent; tokens obtained later by refresh carry the grant’s full set.
4. Exchange the code
https://coliswift.com/api/oauth/tokenBody application/x-www-form-urlencoded (a JSON body is also read). Authenticate with client_id + client_secret in the body or HTTP Basic (Authorization: Basic base64(client_id:client_secret)); Basic wins when both are sent.
grant_typestringrequiredauthorization_code
codestringrequiredThe
cs_code_…from the callback.redirect_uristringrequiredThe same exact URI used in the authorize request.
code_verifierstringrequiredThe verifier whose S256 hash you sent as
code_challenge.
curl -X POST "https://coliswift.com/api/oauth/token" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=$CODE" \
--data-urlencode "redirect_uri=https://app.example.com/coliswift/callback" \
--data-urlencode "code_verifier=$CODE_VERIFIER" \
--data-urlencode "client_id=$COLISWIFT_CLIENT_ID" \
--data-urlencode "client_secret=$COLISWIFT_CLIENT_SECRET"const res = await fetch('https://coliswift.com/api/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: process.env.CODE,
redirect_uri: 'https://app.example.com/coliswift/callback',
code_verifier: process.env.CODE_VERIFIER,
client_id: process.env.COLISWIFT_CLIENT_ID,
client_secret: process.env.COLISWIFT_CLIENT_SECRET,
}),
});
const tokens = await res.json();$ch = curl_init('https://coliswift.com/api/oauth/token');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => http_build_query([
'grant_type' => 'authorization_code',
'code' => getenv('CODE'),
'redirect_uri' => 'https://app.example.com/coliswift/callback',
'code_verifier' => getenv('CODE_VERIFIER'),
'client_id' => getenv('COLISWIFT_CLIENT_ID'),
'client_secret' => getenv('COLISWIFT_CLIENT_SECRET'),
]),
]);
$tokens = json_decode(curl_exec($ch), true);import os, requests
res = requests.post("https://coliswift.com/api/oauth/token", data={
"grant_type": "authorization_code",
"code": os.environ["CODE"],
"redirect_uri": "https://app.example.com/coliswift/callback",
"code_verifier": os.environ["CODE_VERIFIER"],
"client_id": os.environ["COLISWIFT_CLIENT_ID"],
"client_secret": os.environ["COLISWIFT_CLIENT_SECRET"],
})
tokens = res.json(){
"access_token": "cs_at_…",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "cs_rt_…",
"scope": "profile:read catalogue:read orders:write orders:read",
"seller_id": "6a1c2f0e-3b7d-4c55-9e10-2f8a4b7c9d01",
"seller_name": "Atlas Beauty"
}access_tokenstringcs_at_…. Send asAuthorization: Bearer …on every API call.token_typestringBearer
expires_inintegerSeconds until the access token expires.
refresh_tokenstringcs_rt_…. Single use.scopestringSpace-separated scopes this token carries.
seller_idstringThe ColiSwift seller this connection belongs to. Use it as your key; it is also
seller_idin every webhook envelope.seller_namestringCompany name, else first + last name.
Responses carry Cache-Control: no-store. Tokens are opaque: do not parse them.
5. Refresh
Same endpoint, grant_type=refresh_token. Every refresh returns a new pair; the refresh token you sent is spent. Store the new one before using the new access token.
curl -X POST "https://coliswift.com/api/oauth/token" \
--data-urlencode "grant_type=refresh_token" \
--data-urlencode "refresh_token=$REFRESH_TOKEN" \
--data-urlencode "client_id=$COLISWIFT_CLIENT_ID" \
--data-urlencode "client_secret=$COLISWIFT_CLIENT_SECRET"const res = await fetch('https://coliswift.com/api/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: process.env.REFRESH_TOKEN,
client_id: process.env.COLISWIFT_CLIENT_ID,
client_secret: process.env.COLISWIFT_CLIENT_SECRET,
}),
});
const tokens = await res.json();$ch = curl_init('https://coliswift.com/api/oauth/token');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => http_build_query([
'grant_type' => 'refresh_token',
'refresh_token' => getenv('REFRESH_TOKEN'),
'client_id' => getenv('COLISWIFT_CLIENT_ID'),
'client_secret' => getenv('COLISWIFT_CLIENT_SECRET'),
]),
]);
$tokens = json_decode(curl_exec($ch), true);import os, requests
res = requests.post("https://coliswift.com/api/oauth/token", data={
"grant_type": "refresh_token",
"refresh_token": os.environ["REFRESH_TOKEN"],
"client_id": os.environ["COLISWIFT_CLIENT_ID"],
"client_secret": os.environ["COLISWIFT_CLIENT_SECRET"],
})
tokens = res.json()6. Revoke (disconnect)
https://coliswift.com/api/oauth/revokeSend token = an access or a refresh token. The whole connection for that seller is revoked: every access and refresh token of the grant dies. The answer is always 200 {} — also for an unknown token, a token of another app or a wrong secret — so it never confirms which tokens exist.
curl -X POST "https://coliswift.com/api/oauth/revoke" \
--data-urlencode "token=$REFRESH_TOKEN" \
--data-urlencode "client_id=$COLISWIFT_CLIENT_ID" \
--data-urlencode "client_secret=$COLISWIFT_CLIENT_SECRET"const res = await fetch('https://coliswift.com/api/oauth/revoke', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
token: process.env.REFRESH_TOKEN,
client_id: process.env.COLISWIFT_CLIENT_ID,
client_secret: process.env.COLISWIFT_CLIENT_SECRET,
}),
});
const tokens = await res.json();$ch = curl_init('https://coliswift.com/api/oauth/revoke');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => http_build_query([
'token' => getenv('REFRESH_TOKEN'),
'client_id' => getenv('COLISWIFT_CLIENT_ID'),
'client_secret' => getenv('COLISWIFT_CLIENT_SECRET'),
]),
]);
$tokens = json_decode(curl_exec($ch), true);import os, requests
res = requests.post("https://coliswift.com/api/oauth/revoke", data={
"token": os.environ["REFRESH_TOKEN"],
"client_id": os.environ["COLISWIFT_CLIENT_ID"],
"client_secret": os.environ["COLISWIFT_CLIENT_SECRET"],
})
tokens = res.json()A seller can also disconnect your app from their ColiSwift Integrations page, and ColiSwift revokes every connection when an app is deactivated. You are not notified by webhook: your next API call answers 401 INVALID_TOKEN, and the refresh answers invalid_grant. Treat both as « the seller must connect again ».
While a seller’s ColiSwift account is deactivated, every API call with their tokens answers 401 INVALID_TOKEN and no event is sent for them. The connection itself is not revoked: if ColiSwift reactivates the account, the connection works again.
Token endpoint errors
The token and revoke endpoints answer in the RFC 6749 §5.2 shape, not the API’s error envelope:
{
"error": "invalid_grant",
"error_description": "The authorization code has expired."
}| HTTP | error | When |
|---|---|---|
| 401 | invalid_client | Missing, unknown or wrong client credentials, or the app is deactivated (same message for every case). With Basic auth a WWW-Authenticate: Basic realm="oauth" header is added. |
| 400 | invalid_request | code, redirect_uri or code_verifier missing; refresh_token missing. |
| 400 | invalid_grant | Code or refresh token unknown, expired, already used, issued to another app; grant revoked; redirect_uri different from the authorize one; verifier does not match the challenge. |
| 400 | unsupported_grant_type | Anything but authorization_code or refresh_token. |
| 429 | — | More than 60 requests per minute for one client id from one address. This one uses the API envelope {"error":{"code":"RATE_LIMITED",…}}. |

