Account linking and merging API (token exchange, /me/identities, /account/merge)

OAuth Token exchange LINE Apple Google Account merge iOS Android

This guide covers the token exchange that signs an app in with an ID token from LINE, Apple or Google, the /api/v1/me/identities endpoints for adding and removing sign-in methods, and /api/v1/account/merge for merging two accounts. These are the same endpoints the ReceiptRoller iOS and Android apps and the LINE mini app use.

Token exchange (sign in with an ID token)

An RFC 8693 token exchange turns the ID token your platform SDK returned into a ReceiptRoller access token, without a browser round trip through /oauth/authorize. Only client IDs allowed to use token exchange can call it — contact support if you need it.

POST https://receiptroller.io/api/v1/auth/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&client_id={client_id}
&subject_token={ID token}
&subject_token_type=urn:ietf:params:oauth:token-type:id_token
&subject_issuer=line            # line | apple | google
&organization_id={business account id}  # only for a store's own LINE channel
&nonce={nonce}                  # if you gave the SDK a nonce
&create_if_missing=true         # create an account when none uses this identity

A successful response is the usual token plus issued_token_type and account_created (true when this call created the account).

StatuserrorMeaning
400invalid_requestsubject_token, subject_token_type or subject_issuer missing or wrong
401invalid_clientThe client isn't allowed to use token exchange
400invalid_grantThe ID token couldn't be verified (expired, another channel, nonce mismatch…)
404identity_not_foundNo account uses this identity. When an account with the same verified email exists, candidate_account (masked_email, sign_in_methods) is included — ask the person to sign in to it and link from there
429rate_limit_exceededToo many accounts created from the same address

Proof

Adding or removing a sign-in method and merging also need proof that the caller owns the account, on top of the token.

{ "type": "password", "password": "..." }
{ "type": "id_token", "issuer": "line", "idToken": "...", "nonce": null, "organizationId": null }
  • Use one of the account's own sign-in methods: its password, or a LINE, Apple or Google identity connected to it
  • An account with no sign-in method yet (started anonymously in the phone app) can omit proof
  • Missing proof answers 403 reauth_required, wrong proof 403 proof_rejected; both list what's accepted in acceptedMethods

Sign-in methods: /api/v1/me/identities

  • GET /api/v1/me/identities: email, hasPassword and identities (id, provider, scope, linkedAt, lastUsedAt, canRemove)
  • POST /api/v1/me/identities: { issuer, idToken, nonce?, organizationId?, proof } adds a LINE, Apple or Google identity. If another account already uses it, the answer is 409 identity_belongs_to_other_account with a mergeTicket
  • DELETE /api/v1/me/identities/{id}: body { proof }, using a method other than the one being removed. The last method answers 409 last_sign_in_method
  • POST /api/v1/me/identities/password/code: { email, proof } sends a code to the email (202). Codes per hour are limited (429 rate_limit_exceeded)
  • POST /api/v1/me/identities/password: { email, password, code, proof }. If another account uses that email, 409 identity_belongs_to_other_account with a mergeTicket; a password that fails the rules is 400 password_rejected

Merging accounts: /api/v1/account/merge

A merge needs a mergeTicket (valid 10 minutes) that carries the proof of the other account. A ticket only works with the token of the account that obtained it.

  1. Prove the other account and get a ticket: POST /api/v1/account/merge/tickets
    { "proof": {...},
      "other": { "type": "email_code", "email": "...", "code": "..." } }
    // type is id_token, password or email_code
    Send the code with POST /api/v1/account/merge/email-code ({ email, proof }). It answers 202 whether or not an account uses the address.
  2. Preview: POST /api/v1/account/merge/preview ({ mergeTicket, keep? }). keep is current (keep the account signed in) or other; omitted, suggestedKeep is used — the account the person made themselves, or the older of two of a kind
  3. Merge: POST /api/v1/account/merge ({ mergeTicket, keep, confirm: true }). 202 returns the mergeId and tokens for the account kept — switch to them. Sending the same ticket again returns the same merge
  4. Progress: GET /api/v1/account/merge/{mergeId}. status is running, completed, failed or undone. When canResume is true, POST /api/v1/account/merge/{mergeId}/resume picks it up again

POST /api/v1/account/merge/suggestions ({ proof }) returns another account that is probably the person's. same_line is an account with the same LINE id; LINE proves both, so it comes with a mergeTicket. same_email is an account whose email matches the one in the LINE ID token; it has no ticket and must be proven as usual.

StatuserrorMeaning
400invalid_credentials / invalid_code / invalid_grantThe other account couldn't be proven (an unknown email answers the same way)
400same_accountThe other account is the one signed in
400invalid_ticket / confirm_requiredTicket expired or invalid / confirm isn't true
403ticket_not_yoursAnother account obtained the ticket
404other_account_not_found / merge_not_foundThe other account or the merge doesn't exist
409already_merged / not_resumableAlready merged / can't be resumed
429locked_out / rate_limit_exceededToo many wrong passwords / too many codes

Implementation notes

  • Branch on the error code. The wording of errorDescription can change
  • Adding or removing a sign-in method and merging are notified to the person by email and LINE; your app doesn't need to send its own notice
  • A merge can't be undone from an app. Within 30 days, support may be able to separate the accounts again

Related

Published: 2026-10-01 Updated: 2026-10-01
Tags
API (22) OAuth (16) Android (11) iOS (9) Webhook (5) App registration (4) POS Integration (4) Reference (4) Troubleshooting (4) api (4)